﻿/* Copyright (c) 2010 Lexalytics Inc.

 Permission is hereby granted, free of charge, to any person
 obtaining a copy of this software and associated documentation
 files (the "Software"), to deal in the Software without
 restriction, including without limitation the rights to use,
 copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the
 Software is furnished to do so, subject to the following
 conditions:

 The above copyright notice and this permission notice shall be
 included in all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 OTHER DEALINGS IN THE SOFTWARE.
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;

// Needed for Office and Excel integration
using Excel = Microsoft.Office.Interop.Excel;

// Needed for Lexalytics business classes
using LexaScopeNET;

namespace Lexalytics4Excel.Dialogs
{
    /// <summary>
    /// Dialog to provide progress information on data load process
    /// </summary>
    public partial class ProcessDataProgress : Form
    {
        private LXAWebService wsLXA = new LXAWebService();
        private string _ServiceURL = "";
        private string _PublicKey = "";
        private string _PrivateKey = "";
        private string _ProjectKey = "";
        private Excel.Range _DataRange;
        private bool _IncludesHeader = false;
        private int _TextColumnIndex = 1;
        private bool _MultiColumn = false;
        private bool _WriteLog = false;

        /// <summary>Property indicating multiple columns detected in the data range</summary>
        public bool MultiColumnDetected
        {
            get { return _MultiColumn; }
        }
        private Hashtable _DataHashtable = new Hashtable();
        /// <summary>Property access to hashtable of project assets</summary>
        public Hashtable DataHashes
        {
            get { return _DataHashtable; }
        }

        private int _nMinRow = 1;
        private int _nMaxRow = 1;
        private DateTime _dtStartTime = new DateTime();
        
        /// <summary>
        /// Constructor, initializes member variables
        /// </summary>
        /// <param name="ServiceURL">Base URL for web service calls</param>
        /// <param name="PublicKey">User public key</param>
        /// <param name="PrivateKey">User private key</param>
        /// <param name="ProjectKey">Identifier for project to add content to</param>
        /// <param name="DataHashtable">Hashtable of project assets</param>
        /// <param name="DataRange">Excel range selected by user</param>
        /// <param name="IncludesHeader">Flag indicating whether range include a header row</param>
        /// <param name="TextColumnIndex">Indicator of text column</param>
        /// <param name="WriteLog">Flag to write to log</param>
        public ProcessDataProgress(string ServiceURL, string PublicKey, string PrivateKey, string ProjectKey, Hashtable DataHashtable, Excel.Range DataRange, bool IncludesHeader, int TextColumnIndex, bool WriteLog)
        {
            _ServiceURL = ServiceURL;
            _PublicKey = PublicKey;
            _PrivateKey = PrivateKey;
            _ProjectKey = ProjectKey;
            _DataHashtable = DataHashtable;
            _DataRange = DataRange;
            _IncludesHeader = IncludesHeader;
            _TextColumnIndex = TextColumnIndex;
            _WriteLog = WriteLog;

            InitializeComponent();
        }

        #region Form event handlers

        /// <summary>
        /// Form load event. Determine how many rows we have and kick off thread.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ProcessDataProgress_Load(object sender, EventArgs e)
        {
            // Start the clock
            _dtStartTime = DateTime.Now;
            // Set all status to INACTIVE
            Hashtable tempHashtable = new Hashtable();
            foreach (string strDataHash in _DataHashtable.Keys)
            {
                Document myDoc = (Document)_DataHashtable[strDataHash];
                myDoc.DataStatus = "INACTIVE";
                tempHashtable.Add(strDataHash, myDoc);
            }
            _DataHashtable = tempHashtable;

            thdProcess.RunWorkerAsync();
        }


        /// <summary>
        /// Quit button handler. Requests the thread cancel if it is processing, otherwise close dialog.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void QuitButton_Click(object sender, EventArgs e)
        {
            if (thdProcess.IsBusy)
                thdProcess.CancelAsync();
            else
                this.Close();
        }

        #endregion // Form event handlers

        #region Background worker thread methods
        private void thdProcess_DoWork(object sender, DoWorkEventArgs e)
        {
            string strStatus = "";
            string strContent = "";
            string strHash = "";

            int nDocsProcessed = 0;
            int nLastPercentComplete = 0;
            int nLoadFileDocs = 0;
            int kMaxLoadFiles = 10;

            int keyRow = 1;
            if (_IncludesHeader)
                keyRow++;

            string strRange_R1C1 = "";

            // Iterate through the selected data range and add data to project
            try
            {
                _nMinRow = 1;
                _nMaxRow = 1;
                strRange_R1C1 = _DataRange.get_Address(Type.Missing, _TextColumnIndex, Excel.XlReferenceStyle.xlR1C1, Type.Missing, Type.Missing);
                string[] arrBounds = strRange_R1C1.Split(new char[] { 'R', 'C', ':' }, StringSplitOptions.RemoveEmptyEntries);
                switch (arrBounds.GetLength(0))
                {
                    case 4:
                        _nMinRow = Convert.ToInt32(arrBounds[0]);
                        _nMaxRow = Convert.ToInt32(arrBounds[2]);
                        break;
                    default:
                        _nMaxRow = _DataRange.SpecialCells(Excel.XlCellType.xlCellTypeLastCell, Type.Missing).Row;
                        break;
                }

                // Set max docs for loadfile based on number of expected rows
                kMaxLoadFiles = (_nMaxRow - _nMinRow) / 10;
                if (kMaxLoadFiles < 10)
                    kMaxLoadFiles = 10;
                if (kMaxLoadFiles > 100)
                    kMaxLoadFiles = 100;

                // Start at the beginning of the range
                if (_IncludesHeader)
                    _nMinRow++;
                keyRow = _nMinRow;
                Excel.Range _rng = (Excel.Range)_DataRange.Cells[keyRow, _TextColumnIndex];

                // string to contain XML loadfile contents
                StringBuilder sbXmlLoadFile = new StringBuilder();

                while (keyRow <= _nMaxRow)
                {
                    // Start off the loadfile
                    if (nLoadFileDocs == 0)
                    {
                        string strIdentifier = System.Guid.NewGuid().ToString();
                        sbXmlLoadFile.AppendFormat("<tempest_loadfile><publickey>{0}</publickey><identifier>{1}</identifier>", _PublicKey, strIdentifier);
                    }

                    nDocsProcessed++;
                    if ((_rng.Value2 != null) && (_rng.Value2.ToString().Length > 0))
                    {
                        strContent = _rng.Value2.ToString();
                        strHash = GetDataHash(strContent);

                        if (!_DataHashtable.Contains(strHash))
                        {
                            // Start off item
                            sbXmlLoadFile.Append("<asset>");
                            // Add metadata for item to loadfile
                            AddMetadataToLoadfile(strHash, keyRow, _DataRange, _IncludesHeader, _TextColumnIndex, ref sbXmlLoadFile);

                            // Add loadfile to current item node
                            sbXmlLoadFile.AppendFormat("<content><![CDATA[{0}]]></content>", strContent);

                            // End item
                            sbXmlLoadFile.Append("</asset>");

                            _DataHashtable.Add(strHash, new Document()
                            {
                                DataHash = strHash,
                                DataStatus = strStatus,
                                Identifier = keyRow.ToString(),
                            });
                        }
                        else
                        {
                            strStatus = "ACTIVE";
                            _DataHashtable[strHash] = new Document()
                            {
                                DataHash = strHash,
                                DataStatus = strStatus,
                                Identifier = keyRow.ToString(),
                            };
                        }
                    }

                    // Report progress
                    if (thdProcess.CancellationPending)
                    {
                        e.Cancel = true;
                        break;
                    }
                    else
                    {
                        float fPercent = ((float)nDocsProcessed / (float)(_nMaxRow-_nMinRow));                         
                        int percentComplete =  (int)(fPercent * 100.0);
                        if (((nDocsProcessed % 10 == 0) || percentComplete>nLastPercentComplete) && (percentComplete<=100))
                        {
                            thdProcess.ReportProgress(percentComplete, String.Format("INFO|{0}", nDocsProcessed));
                            nLastPercentComplete = percentComplete;
                        }
                    }

                    // Check to see if loadfile has reached threshold to send
                    nLoadFileDocs++;
                    if (nLoadFileDocs == kMaxLoadFiles || keyRow >= _nMaxRow)
                    {
                        sbXmlLoadFile.Append("</tempest_loadfile>");
                        // Send the loadfile
                        strStatus = wsLXA.WSAddLoadfile(_ServiceURL, _PublicKey, _PrivateKey, _ProjectKey, sbXmlLoadFile.ToString());
                        // Reset the loadfile
                        sbXmlLoadFile = new StringBuilder();
                        nLoadFileDocs = 0;
                    }

                    // Process the next row
                    keyRow++;
                    
                    _rng = (Excel.Range)_DataRange.Cells[keyRow, _TextColumnIndex];
                }
            }
            catch (Exception err)
            {
#if DEBUG
                System.Diagnostics.Debug.WriteLine(err);
#endif
                if (_WriteLog)
                    Logger.Instance.WriteToLog(err);
                thdProcess.ReportProgress(100, String.Format("FAIL|{0}|{1}", nDocsProcessed, err.Message));
            }
            finally
            {
                thdProcess.ReportProgress(100, String.Format("DONE|{0}", nDocsProcessed));
            }

        }

        private void thdProcess_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            pbProcess.Value = e.ProgressPercentage;
            if (e.UserState != null)
            {
                // Update informational progress message, elapsed time and if applicable estimated time remaining
                ElapsedLabel.Text = String.Format("Time elapsed: {0}", GetTimeElapsed());
                string[] msgArray = e.UserState.ToString().Split('|');
                switch (msgArray[0])
                {
                    case "INFO":
                        ProgressLabel.Text = String.Format("{0} docs processed...", msgArray[1]);
                        RemainingLabel.Text = String.Format("Time remaining: {0} (estimated)", GetTimeRemaining(Convert.ToInt32(msgArray[1])));
                        break;
                    case "FAIL":
                        ProgressLabel.Text = String.Format("Error: {0} rows processed before error occurred:\n{1}", msgArray[1], msgArray[2]);
                        RemainingLabel.Text = "Time remaining: 00:00:00";
                        break;
                    case "DONE":
                        ProgressLabel.Text = String.Format("Complete: {0} rows processed...", msgArray[1]);
                        RemainingLabel.Text = "Time remaining: 00:00:00";
                        break;
                    default:
                        ProgressLabel.Text = e.UserState.ToString();
                        RemainingLabel.Text = "";
                        break;
                }
            }
        }


        private void thdProcess_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            // Remove content still marked with STATUS1
            Hashtable tempHashtable = new Hashtable();
            foreach (string strDataHash in _DataHashtable.Keys)
            {
                Document aDoc = (Document)_DataHashtable[strDataHash];
                string strStatus = aDoc.DataStatus;
                if (strStatus.Equals("INACTIVE")||strStatus.Equals("ERROR"))
                    wsLXA.WSDeleteDocument(_ServiceURL, _PublicKey, _PrivateKey, _ProjectKey, strDataHash);
                else
                    tempHashtable.Add(strDataHash, aDoc);
            }
            _DataHashtable = tempHashtable;

            QuitButton.Text = "&OK";
        }


        #endregion // Background worker thread methods

        /// <summary>
        /// Performs the internal work of adding metadata for a row of content.
        /// </summary>
        /// <param name="strHash">Asset identifier</param>
        /// <param name="keyRow">Row number in data sheet</param>
        /// <param name="DataRange">Access to complete DataRange</param>
        /// <param name="IncludesHeader">Indicator that DataRange includes header row</param>
        /// <param name="TextColumnIndex">Column number for text column</param>
        private void ProcessMetadata(string strHash, int keyRow, Excel.Range DataRange, bool IncludesHeader, int TextColumnIndex)
        {
            // Add plug-in specific metadata of row index and status
            int nReturn = wsLXA.WSAddMetadata(_ServiceURL, _PublicKey, _PrivateKey, _ProjectKey, strHash, "rowindex", keyRow.ToString());

            // Set multi-column flag to false until otherwise notified
            _MultiColumn = false;

            // Determine number of columns
            int nMinCol = 1;
            int nMaxCol = 1;
            string strRange_R1C1 = DataRange.get_Address(keyRow, Type.Missing, Excel.XlReferenceStyle.xlR1C1, Type.Missing, Type.Missing);
            string[] arrBounds = strRange_R1C1.Split(new char[] { 'R', 'C', ':' }, StringSplitOptions.RemoveEmptyEntries);
            switch (arrBounds.GetLength(0))
            {
                case 4:
                    nMinCol = Convert.ToInt32(arrBounds[1]);
                    nMaxCol = Convert.ToInt32(arrBounds[3]);
                    break;
                default:
                    nMaxCol = DataRange.SpecialCells(Excel.XlCellType.xlCellTypeLastCell, Type.Missing).Column;
                    break;
            }

            // Iterate across columns in the DataRange for the current row and determine headers
            SortedList<int, string> lstColHeaders = new SortedList<int, string>();
            for (int nCol = nMinCol; nCol <= nMaxCol; nCol++)
            {
                string strColHeader = "";
                if (IncludesHeader)
                {
                    Excel.Range rHeaderCell = (Excel.Range)DataRange.Cells[1, nCol];
                    if ((rHeaderCell.Value2 != null) && (rHeaderCell.Value2.ToString().Length > 0))
                    {
                        strColHeader = rHeaderCell.Value2.ToString();
                    }
                    else
                    {
                        strColHeader = String.Format("Column{0}", nCol);
                    }
                }
                else
                {
                    strColHeader = String.Format("Column{0}", nCol);
                }
                lstColHeaders.Add(nCol, strColHeader);
            }

            // Iterate across column in row provided. 
            // If not the text column, add metadata for asset identified by hash
            for (int nCol = nMinCol; nCol <= nMaxCol; nCol++)
            {
                // Insert a slight pause
                System.Threading.Thread.Sleep(10);
                if (nCol != TextColumnIndex)
                {
                    string strMetadata = "";
                    Excel.Range rMetadataCell = (Excel.Range)DataRange.Cells[keyRow, nCol];
                    if ((rMetadataCell.Value2 != null) && (rMetadataCell.Value2.ToString().Length > 0))
                    {
                        strMetadata = rMetadataCell.Value2.ToString();
                        int nRet = -1;
                        nRet = wsLXA.WSAddMetadata(_ServiceURL, _PublicKey, _PrivateKey, _ProjectKey, strHash, lstColHeaders[nCol], strMetadata);
                        _MultiColumn = (nRet == 0);
                    }
                }
            }
        }

        /// <summary>
        /// Performs the internal work of adding metadata for a row of content.
        /// </summary>
        /// <param name="strHash">Asset identifier</param>
        /// <param name="keyRow">Row number in data sheet</param>
        /// <param name="DataRange">Access to complete DataRange</param>
        /// <param name="IncludesHeader">Indicator that DataRange includes header row</param>
        /// <param name="TextColumnIndex">Column number for text column</param>
        /// <param name="sbXmlLoadFile">Loadfile to which metadata should be appended</param>
        private void AddMetadataToLoadfile(string strHash, int keyRow, Excel.Range DataRange, bool IncludesHeader, int TextColumnIndex, ref StringBuilder sbXmlLoadFile)
        {
            // Add plug-in specific metadata of row index and status
            sbXmlLoadFile.AppendFormat("<metadata><label>rowindex</label><value>{0}</value></metadata>",keyRow);

            // Set multi-column flag to false until otherwise notified
            _MultiColumn = false;

            // Determine number of columns
            int nMinCol = 1;
            int nMaxCol = 1;
            string strRange_R1C1 = DataRange.get_Address(keyRow, Type.Missing, Excel.XlReferenceStyle.xlR1C1, Type.Missing, Type.Missing);
            string[] arrBounds = strRange_R1C1.Split(new char[] { 'R', 'C', ':' }, StringSplitOptions.RemoveEmptyEntries);
            switch (arrBounds.GetLength(0))
            {
                case 4:
                    nMinCol = Convert.ToInt32(arrBounds[1]);
                    nMaxCol = Convert.ToInt32(arrBounds[3]);
                    break;
                default:
                    nMaxCol = DataRange.SpecialCells(Excel.XlCellType.xlCellTypeLastCell, Type.Missing).Column;
                    break;
            }

            // Iterate across columns in the DataRange for the current row and determine headers
            SortedList<int, string> lstColHeaders = new SortedList<int, string>();
            for (int nCol = nMinCol; nCol <= nMaxCol; nCol++)
            {
                string strColHeader = "";
                if (IncludesHeader)
                {
                    Excel.Range rHeaderCell = (Excel.Range)DataRange.Cells[1, nCol];
                    if ((rHeaderCell.Value2 != null) && (rHeaderCell.Value2.ToString().Length > 0))
                    {
                        strColHeader = rHeaderCell.Value2.ToString();
                    }
                    else
                    {
                        strColHeader = String.Format("Column{0}", nCol);
                    }
                }
                else
                {
                    strColHeader = String.Format("Column{0}", nCol);
                }
                lstColHeaders.Add(nCol, strColHeader);
            }

            // Iterate across column in row provided. 
            // If not the text column, add metadata for asset identified by hash
            for (int nCol = nMinCol; nCol <= nMaxCol; nCol++)
            {
                if (nCol != TextColumnIndex)
                {
                    string strMetadata = "";
                    Excel.Range rMetadataCell = (Excel.Range)DataRange.Cells[keyRow, nCol];
                    if ((rMetadataCell.Value2 != null) && (rMetadataCell.Value2.ToString().Length > 0))
                    {
                        strMetadata = rMetadataCell.Value2.ToString();
                        sbXmlLoadFile.AppendFormat("<metadata><label>{0}</label><value>{1}</value></metadata>", lstColHeaders[nCol], strMetadata);
                        _MultiColumn = true;
                    }
                }
            }
        }

        /// <summary>
        /// Basic use of MD5 as hashcode for content
        /// </summary>
        /// <param name="InputStr">Input string</param>
        /// <returns>Hashcode for content signature</returns>
        private string GetDataHash(string InputStr)
        {
            byte[] textBytes = System.Text.Encoding.Default.GetBytes(InputStr);
            try
            {
                System.Security.Cryptography.MD5CryptoServiceProvider cryptHandler;
                cryptHandler = new System.Security.Cryptography.MD5CryptoServiceProvider();
                byte[] hash = cryptHandler.ComputeHash(textBytes);
                string outputStr = "";
                foreach (byte a in hash)
                {
                    if (a < 16)
                        outputStr += "0" + a.ToString("x");
                    else
                        outputStr += a.ToString("x");
                }
                return outputStr;
            }
            catch
            {
                throw;
            }

        }

        /// <summary>
        /// Return a formatted string of HH:MM:SS of time elapsed
        /// </summary>
        /// <returns>String in format HH:MM:SS</returns>
        private string GetTimeElapsed()
        {
            string strElapsedTime = "00:00:00";
            TimeSpan tsElapsed = new TimeSpan();
            tsElapsed = DateTime.Now - _dtStartTime;
            strElapsedTime = String.Format("{0}:{1}:{2}",
                                            tsElapsed.Hours.ToString("00"), 
                                            tsElapsed.Minutes.ToString("00"), 
                                            tsElapsed.Seconds.ToString("00"));
            return strElapsedTime;
        }

        /// <summary>
        /// Calculates an estimate of the time remaining based on time elapsed and number of rows processed
        /// </summary>
        /// <param name="nProcessed">Current rows processed</param>
        /// <returns>String in format HH:MM:SS</returns>
        private string GetTimeRemaining(int nProcessed)
        {
            int nTotalRows = _nMaxRow - _nMinRow;

            string strTimeLeft = "00:00:00";
            if (nProcessed > 0)
            {
                TimeSpan tsElapsed = new TimeSpan();
                tsElapsed = DateTime.Now - _dtStartTime;
                long lTickPerDoc = tsElapsed.Ticks / (long)nProcessed;
                TimeSpan tsEstimated = new TimeSpan(lTickPerDoc*(nTotalRows-nProcessed));
                strTimeLeft = String.Format("{0}:{1}:{2}",
                                             tsEstimated.Hours.ToString("00"),
                                             tsEstimated.Minutes.ToString("00"),
                                             tsEstimated.Seconds.ToString("00"));
            }
            return strTimeLeft;
        }
    }
}
